﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace FinMkts.Messaging.Pgm
{
    public class BlockBufferedBlockingQueue
    {
        private readonly object _gate = new object();
        private readonly Action<ByteBufferCollection> _callback;
        private readonly Queue<byte[]> _queue = new Queue<byte[]>();
        private readonly int _blockSizeInBytes;
        private int _queueBytes;

        private bool _isBlocked;

        public int Count
        {
            get { return _queue.Count; }
        }

        /// <summary>
        /// Is the queue currently blocked
        /// </summary>
        public bool IsBlocked
        {
            get { return _isBlocked; }
            set
            {
                lock(_gate)
                {
                    _isBlocked = value;
                }
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="blockSize"></param>
        /// <param name="callback"></param>
        public BlockBufferedBlockingQueue(BinarySize blockSize, Action<ByteBufferCollection> callback)
        {
            if (callback == null) throw new ArgumentNullException("callback");
            if (blockSize.Bytes == 0) throw new ArgumentOutOfRangeException("blockSize", blockSize, "blockSize must be greater than zero");
            _blockSizeInBytes = (int) blockSize.Bytes;
            _callback = callback;
        }

        /// <summary>
        /// Add a single message
        /// </summary>
        /// <param name="data"></param>
        public void Add(byte[] data)
        {
            if (data == null) throw new ArgumentNullException("data");
            if (data.Length == 0) return;   // nothing to do
            if (data.Length > _blockSizeInBytes) throw new ArgumentOutOfRangeException("data", "single message is greater than max size");
            lock(_gate)
            {
                // just add to queue if we are blocked
                if (IsBlocked)
                {
                    while (_queueBytes + data.Length > _blockSizeInBytes)
                    {
                        Monitor.Wait(_gate);
                    }
                    // queue might no be blocked after the wait
                    if (IsBlocked)
                    {
                        _queue.Enqueue(data);
                        _queueBytes += data.Length;
                        return;
                    }
                }

                // fill up a buffer as much as we can
                var buffer = new ByteBufferCollection {new ArraySegment<byte>(data)};

                Debug.Assert(buffer.TotalBytes > 0, "buffer is empty");
                Debug.Assert(buffer.TotalBytes < _blockSizeInBytes, "buffer too full");

                // do the buffer callback
                _callback(buffer);
            }
        }

        public ByteBufferCollection TakeNextBlock()
        {
            lock (_gate)
            {
                if (_queue.Count == 0) return null;

                // get all items from the queue
                var buffer = new ByteBufferCollection();
                buffer.AddRange(_queue);
                _queue.Clear();
                _queueBytes = 0;

                // single to "Add" that the queue is no longer full
                Monitor.Pulse(_gate);

                return buffer;
            }
        }

    }
}